﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.Reflection;
using System.Reflection.Emit;

namespace Phoenix.Xna.Framework.Reflection
{
    public delegate object GenericInvoker(object target, params object[] arguments);

    public static class ReflectionHelper
    {
        public static V GetField<T, V>(T instance, string name)
        {
            V value = (V)typeof(T).InvokeMember(name,
                BindingFlags.GetField | BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public,
                null,
                instance,
                null);
            return value;
        }

        public static void SetField<T>(T instance, string field, object value)
        {
            typeof(T).InvokeMember(field,
                BindingFlags.SetField | BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static,
                null,
                instance,
                new object[] { value });
        }

        public static void SetField(object instance, string field, object value)
        {
            Type t = instance.GetType();
            FieldInfo fi = t.GetField(field, BindingFlags.SetField | BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static);
            fi.SetValue(instance, ConversionHelper.Convert(fi.FieldType, value));
        }

        public static T CreateInstance<T>()
        {
            Type[] pTypes = new Type[0];
            ConstructorInfo c = typeof(T).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public, null, pTypes, null);
            T instance = (T)c.Invoke(BindingFlags.NonPublic | BindingFlags.Public, null, null, CultureInfo.CurrentCulture);
            return instance;
        }

        public static void InvokeMethod<T>(T instance, string method)
        {
            MethodInfo mi = instance.GetType().GetMethod(method);
            mi.Invoke(instance, null);
        }

        public static void InvokeMethod<T>(T instance, string method, List<object> parameters)
        {
            MethodInfo mi = instance.GetType().GetMethod(method);
            mi.Invoke(instance, parameters.ToArray());
        }

        public static void SetProperty<T>(T instance, string property, object value)
        {
            typeof(T).InvokeMember(property,
                BindingFlags.SetProperty | BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static,
                null,
                instance,
                new object[] { value });
        }

        public static void SetProperty(object instance, string property, object value)
        {
            PropertyInfo pi = instance.GetType().GetProperty(property);
            pi.SetValue(instance, ConversionHelper.Convert(pi.PropertyType, value), null);
        }

        public static GenericInvoker GenericMethodInvokerMethod(Type type, string methodName, Type[] typeArguments, Type[] parameterTypes)
        {
            MethodInfo methodInfo;
            ParameterInfo[] parameters;
            FindMethod(type, methodName, typeArguments, parameterTypes, out methodInfo, out parameters);
            string name = string.Format("__MethodInvoker_{0}_ON_{1}", methodInfo.Name, methodInfo.DeclaringType.Name);
            DynamicMethod dynamicMethod = new DynamicMethod(name, 
                typeof(object), 
                new Type[] { typeof(object), typeof(object[]) },
                methodInfo.DeclaringType);
            ILGenerator generator = dynamicMethod.GetILGenerator();
            generator.DeclareLocal(typeof(object));
            generator.Emit(OpCodes.Ldarg_0);
            generator.Emit(OpCodes.Castclass, methodInfo.DeclaringType);

            for (int i = 0; i < parameters.Length; i++)
            {
                generator.Emit(OpCodes.Ldarg_1);
                generator.Emit(OpCodes.Ldc_I4, i);
                generator.Emit(OpCodes.Ldelem_Ref);
                Type parameterType = parameters[i].ParameterType;

                if (parameterType.IsClass)
                {
                    generator.Emit(OpCodes.Castclass, parameterType);
                }
                else
                {
                    generator.Emit(OpCodes.Unbox_Any, parameterType);
                }
            }

            generator.EmitCall(OpCodes.Callvirt, methodInfo, null);

            if (methodInfo.ReturnType == typeof(void))
            {
                generator.Emit(OpCodes.Ldnull);
            }
            else
            {
                if (methodInfo.ReturnType.IsValueType)
                {
                    generator.Emit(OpCodes.Box, methodInfo.ReturnType);
                }
            }

            generator.Emit(OpCodes.Stloc_0);
            generator.Emit(OpCodes.Ldloc_0);
            generator.Emit(OpCodes.Ret);
            return (GenericInvoker)dynamicMethod.CreateDelegate(typeof(GenericInvoker));
        }

        public static GenericInvoker GenericMethodInvokerMethod(Type type, string methodName, Type[] typeArguments)
        {
            return GenericMethodInvokerMethod(type, methodName, typeArguments, null);
        }

        private static void FindMethod(Type type, string methodName, Type[] typeArguments, Type[] parameterTypes, out MethodInfo methodInfo, out ParameterInfo[] parameters)
        {
            methodInfo = null;
            parameters = null;

            if (null == parameterTypes)
            {
                methodInfo = type.GetMethod(methodName, BindingFlags.Public | BindingFlags.Instance);
                methodInfo = methodInfo.MakeGenericMethod(typeArguments);
                parameters = methodInfo.GetParameters();
            }
            else
            {
                MethodInfo[] methods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance);

                foreach (MethodInfo method in methods)
                {
                    if (method.Name == methodName)
                    {
                        MethodInfo genericMethod = method.MakeGenericMethod(typeArguments);
                        parameters = genericMethod.GetParameters();

                        if (parameters.Length == parameterTypes.Length)
                        {
                            for (int i = 0; i < parameters.Length; i++)
                            {
                                if (parameters[i].ParameterType != parameterTypes[i])
                                {
                                    continue;
                                }
                            }

                            methodInfo = genericMethod;
                            break;
                        }
                    }
                }

                if (null == methodInfo)
                {
                    throw new InvalidOperationException("Method not found");
                }
            }
        }
    }
}
